import ProcessExecution from './ProcessExecution.js';
import { getUniqueId } from '../shared.js';
import { ProcessApi } from '../Api.js';
import { ProcessBroker } from '../EventBroker.js';
import { cloneMessage, cloneContent, cloneParent } from '../messageHelper.js';
import { makeErrorFromMessage } from '../error/Errors.js';

const kConsuming = Symbol.for('consuming');
const kCounters = Symbol.for('counters');
const kExec = Symbol.for('execution');
const kExecuteMessage = Symbol.for('executeMessage');
const kExtensions = Symbol.for('extensions');
const kLanes = Symbol.for('lanes');
const kMessageHandlers = Symbol.for('messageHandlers');
const kStateMessage = Symbol.for('stateMessage');
const kStatus = Symbol.for('status');
const kStopped = Symbol.for('stopped');

export default Process;

export function Process(processDef, context) {
  const { id, type = 'process', name, parent, behaviour = {} } = processDef;
  this.id = id;
  this.type = type;
  this.name = name;
  this.parent = parent ? cloneParent(parent) : {};
  this.behaviour = behaviour;

  const { isExecutable } = behaviour;
  this.isExecutable = isExecutable;

  const environment = (this.environment = context.environment);
  this.context = context;
  this[kCounters] = {
    completed: 0,
    discarded: 0,
  };
  this[kConsuming] = false;
  this[kExec] = new Map();
  this[kStatus] = undefined;
  this[kStopped] = false;

  const { broker, on, once, waitFor } = ProcessBroker(this);
  this.broker = broker;
  this.on = on;
  this.once = once;
  this.waitFor = waitFor;

  this[kMessageHandlers] = {
    onApiMessage: this._onApiMessage.bind(this),
    onRunMessage: this._onRunMessage.bind(this),
    onExecutionMessage: this._onExecutionMessage.bind(this),
  };

  this.logger = environment.Logger(type.toLowerCase());

  if (behaviour.lanes) {
    this[kLanes] = behaviour.lanes.map((lane) => new lane.Behaviour(this, lane));
  }
  this[kExtensions] = context.loadExtensions(this);
}

Object.defineProperties(Process.prototype, {
  counters: {
    get() {
      return { ...this[kCounters] };
    },
  },
  lanes: {
    get() {
      return this[kLanes]?.slice();
    },
  },
  extensions: {
    get() {
      return this[kExtensions];
    },
  },
  stopped: {
    get() {
      return this[kStopped];
    },
  },
  isRunning: {
    get() {
      if (!this[kConsuming]) return false;
      return !!this.status;
    },
  },
  executionId: {
    get() {
      const exec = this[kExec];
      return exec.get('executionId') || exec.get('initExecutionId');
    },
  },
  execution: {
    get() {
      return this[kExec].get('execution');
    },
  },
  status: {
    get() {
      return this[kStatus];
    },
  },
  activityStatus: {
    get() {
      return this[kExec].get('execution')?.activityStatus || 'idle';
    },
  },
});

Process.prototype.init = function init(useAsExecutionId) {
  const initExecutionId = useAsExecutionId || getUniqueId(this.id);
  this[kExec].set('initExecutionId', initExecutionId);

  this._debug(`initialized with executionId <${initExecutionId}>`);
  this._publishEvent('init', this._createMessage({ executionId: initExecutionId }));
};

Process.prototype.run = function run(runContent) {
  if (this.isRunning) throw new Error(`process <${this.id}> is already running`);

  const exec = this[kExec];
  const executionId = exec.get('initExecutionId') || getUniqueId(this.id);
  exec.delete('initExecutionId');
  exec.set('executionId', executionId);

  const content = this._createMessage({ ...runContent, executionId });

  const broker = this.broker;
  broker.publish('run', 'run.enter', content);
  broker.publish('run', 'run.start', cloneContent(content));
  broker.publish('run', 'run.execute', cloneContent(content));

  this._activateRunConsumers();
};

Process.prototype.resume = function resume() {
  if (this.isRunning) throw new Error(`cannot resume running process <${this.id}>`);
  if (!this.status) return this;

  this[kStopped] = false;

  const content = this._createMessage();
  this.broker.publish('run', 'run.resume', content, { persistent: false });
  this._activateRunConsumers();
  return this;
};

Process.prototype.getState = function getState() {
  return {
    id: this.id,
    type: this.type,
    executionId: this.executionId,
    environment: this.environment.getState(),
    status: this.status,
    stopped: this.stopped,
    counters: this.counters,
    broker: this.broker.getState(true),
    execution: this.execution?.getState(),
  };
};

Process.prototype.recover = function recover(state) {
  if (this.isRunning) throw new Error(`cannot recover running process <${this.id}>`);
  if (!state) return this;

  this[kStopped] = !!state.stopped;
  this[kStatus] = state.status;
  const exec = this[kExec];
  exec.set('executionId', state.executionId);
  this[kCounters] = { ...this[kCounters], ...state.counters };
  this.environment.recover(state.environment);

  if (state.execution) {
    exec.set('execution', new ProcessExecution(this, this.context).recover(state.execution));
  }

  this.broker.recover(state.broker);

  return this;
};

Process.prototype.shake = function shake(startId) {
  if (this.isRunning) return this.execution.shake(startId);
  return new ProcessExecution(this, this.context).shake(startId);
};

Process.prototype.stop = function stop() {
  if (!this.isRunning) return;
  this.getApi().stop();
};

Process.prototype.getApi = function getApi(message) {
  const execution = this.execution;
  if (execution) return execution.getApi(message);
  return ProcessApi(this.broker, message || this[kStateMessage]);
};

Process.prototype.signal = function signal(message) {
  return this.getApi().signal(message, { delegate: true });
};

Process.prototype.cancelActivity = function cancelActivity(message) {
  return this.getApi().cancel(message, { delegate: true });
};

Process.prototype._activateRunConsumers = function activateRunConsumers() {
  this[kConsuming] = true;
  const broker = this.broker;
  const { onApiMessage, onRunMessage } = this[kMessageHandlers];
  broker.subscribeTmp('api', `process.*.${this.executionId}`, onApiMessage, { noAck: true, consumerTag: '_process-api', priority: 100 });
  broker.getQueue('run-q').assertConsumer(onRunMessage, { exclusive: true, consumerTag: '_process-run' });
};

Process.prototype._deactivateRunConsumers = function deactivateRunConsumers() {
  const broker = this.broker;
  broker.cancel('_process-api');
  broker.cancel('_process-run');
  broker.cancel('_process-execution');
  this[kConsuming] = false;
};

Process.prototype._onRunMessage = function onRunMessage(routingKey, message) {
  const { content, fields } = message;

  if (routingKey === 'run.resume') {
    return this._onResumeMessage(message);
  }

  this[kStateMessage] = message;

  switch (routingKey) {
    case 'run.enter': {
      this._debug('enter');

      this[kStatus] = 'entered';
      if (fields.redelivered) break;

      this[kExec].delete('execution');
      this._publishEvent('enter', content);

      break;
    }
    case 'run.start': {
      this._debug('start');
      this[kStatus] = 'start';
      this._publishEvent('start', content);
      break;
    }
    case 'run.execute': {
      const exec = this[kExec];
      this[kStatus] = 'executing';
      const executeMessage = cloneMessage(message);
      let execution = exec.get('execution');
      if (fields.redelivered && !execution) {
        executeMessage.fields.redelivered = undefined;
      }
      this[kExecuteMessage] = message;

      this.broker.getQueue('execution-q').assertConsumer(this[kMessageHandlers].onExecutionMessage, {
        exclusive: true,
        consumerTag: '_process-execution',
      });

      execution = execution || new ProcessExecution(this, this.context);
      exec.set('execution', execution);
      return execution.execute(executeMessage);
    }
    case 'run.error': {
      this[kStatus] = 'errored';
      this._publishEvent(
        'error',
        cloneContent(content, {
          error: fields.redelivered ? makeErrorFromMessage(message) : content.error,
        }),
      );
      break;
    }
    case 'run.end': {
      this[kStatus] = 'end';

      if (fields.redelivered) break;
      this._debug('completed');

      this[kCounters].completed++;

      this.broker.publish('run', 'run.leave', content);

      this._publishEvent('end', content);
      break;
    }
    case 'run.discarded': {
      this[kStatus] = 'discarded';
      if (fields.redelivered) break;

      this[kCounters].discarded++;

      this.broker.publish('run', 'run.leave', content);

      this._publishEvent('discarded', content);
      break;
    }
    case 'run.leave': {
      this[kStatus] = undefined;
      message.ack();
      this._deactivateRunConsumers();
      const { output, ...rest } = content; // eslint-disable-line no-unused-vars
      this._publishEvent('leave', rest);
      return;
    }
  }

  message.ack();
};

Process.prototype._onResumeMessage = function onResumeMessage(message) {
  message.ack();

  const stateMessage = this[kStateMessage];
  switch (stateMessage.fields.routingKey) {
    case 'run.enter':
    case 'run.start':
    case 'run.discarded':
    case 'run.end':
    case 'run.leave':
      break;
    default:
      return;
  }

  if (!stateMessage.fields.redelivered) return;

  this._debug(`resume from ${this.status}`);

  return this.broker.publish('run', stateMessage.fields.routingKey, cloneContent(stateMessage.content), stateMessage.properties);
};

Process.prototype._onExecutionMessage = function onExecutionMessage(routingKey, message) {
  const content = message.content;
  const messageType = message.properties.type;
  message.ack();

  switch (messageType) {
    case 'stopped': {
      return this._onStop();
    }
    case 'error': {
      this.broker.publish('run', 'run.error', content);
      this.broker.publish('run', 'run.discarded', content);
      break;
    }
    case 'discard':
      this.broker.publish('run', 'run.discarded', content);
      break;
    default: {
      this.broker.publish('run', 'run.end', content);
    }
  }

  const executeMessage = this[kExecuteMessage];
  this[kExecuteMessage] = null;
  executeMessage.ack();
};

Process.prototype._publishEvent = function publishEvent(state, content) {
  const eventContent = this._createMessage({ ...content, state });
  this.broker.publish('event', `process.${state}`, eventContent, { type: state, mandatory: state === 'error' });
};

Process.prototype.sendMessage = function sendMessage(message) {
  const messageContent = message?.content;
  if (!messageContent) return;

  let targetsFound = false;
  if (messageContent.target?.id && this.getActivityById(messageContent.target.id)) {
    targetsFound = true;
  } else if (
    messageContent.message &&
    this.getStartActivities({ referenceId: messageContent.message.id, referenceType: messageContent.message.messageType }).length
  ) {
    targetsFound = true;
  }
  if (!targetsFound) return;

  if (!this.status) this.run();
  this.getApi().sendApiMessage(message.properties.type || 'message', cloneContent(messageContent), { delegate: true });
};

Process.prototype.getActivityById = function getActivityById(childId) {
  const execution = this.execution;
  if (execution) return execution.getActivityById(childId);
  return this.context.getActivityById(childId);
};

Process.prototype.getActivities = function getActivities() {
  const execution = this.execution;
  if (execution) return execution.getActivities();
  return this.context.getActivities(this.id);
};

Process.prototype.getStartActivities = function getStartActivities(filterOptions) {
  return this.context.getStartActivities(filterOptions, this.id);
};

Process.prototype.getSequenceFlows = function getSequenceFlows() {
  const execution = this.execution;
  if (execution) return execution.getSequenceFlows();
  return this.context.getSequenceFlows();
};

Process.prototype.getLaneById = function getLaneById(laneId) {
  const lanes = this[kLanes];
  if (!lanes) return;
  return lanes.find((lane) => lane.id === laneId);
};

Process.prototype.getPostponed = function getPostponed(...args) {
  const execution = this.execution;
  if (!execution) return [];
  return execution.getPostponed(...args);
};

Process.prototype._onApiMessage = function onApiMessage(routingKey, message) {
  const messageType = message.properties.type;

  switch (messageType) {
    case 'stop': {
      if (this.execution && !this.execution.completed) return;
      this._onStop();
      break;
    }
  }
};

Process.prototype._onStop = function onStop() {
  this[kStopped] = true;
  this._deactivateRunConsumers();
  return this._publishEvent('stop');
};

Process.prototype._createMessage = function createMessage(override) {
  return {
    id: this.id,
    type: this.type,
    name: this.name,
    executionId: this.executionId,
    parent: { ...this.parent },
    ...override,
  };
};

Process.prototype._debug = function debug(msg) {
  this.logger.debug(`<${this.id}> ${msg}`);
};
